﻿<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">

    <title>Bitwarden Crypto</title>

    <link href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
    <link href="https://fonts.googleapis.com/css?family=Open+Sans:300,400,600,700,300italic,400italic,600italic"
        rel="stylesheet">
    <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" rel="stylesheet">

    <style>
        body {
            padding-bottom: 50px;
        }

        h1 {
            border-bottom: 2px solid #ced4da;
            margin-bottom: 20px;
            padding-bottom: 10px;
            margin-top: 50px;
        }

        h2 {
            font-size: 24px;
        }

        h3 {
            font-size: 18px;
        }

        pre {
            padding: 9.5px;
            line-height: 1.42857143;
            word-break: break-all;
            word-wrap: break-word;
            background-color: #f5f5f5;
            border: 1px solid #ced4da;
            border-radius: 4px;
        }

        section {
            margin-bottom: 50px;
        }
    </style>
</head>

<body>
    <div class="container" id="app">
        <h1>Key Derivation</h1>

        <form>
            <div class="row">
                <div class="col-sm-4">
                    <div class="form-group">
                        <label for="email">Email</label>
                        <input type="email" id="email" class="form-control" v-model="email">
                    </div>
                </div>
                <div class="col-sm-4">
                    <div class="form-group">
                        <label for="masterPassword">Master Password</label>
                        <input type="password" id="masterPassword" class="form-control" v-model="masterPassword">
                    </div>
                </div>
                <div class="col-sm-4">
                    <div class="form-group">
                        <label for="pbkdf2Iterations">Client PBKDF2 Iterations</label>
                        <input type="number" id="pbkdf2Iterations" class="form-control" v-model="pbkdf2Iterations">
                    </div>
                </div>
            </div>
        </form>

        <section>
            <h2>Master Key</h2>
            <pre>{{masterKey.b64}}</pre>
        </section>

        <section>
            <h2>Master Password Hash</h2>
            <pre>{{masterKeyHash.b64}}</pre>
        </section>

        <section>
            <h2>Stretched Master Key</h2>
            <pre>{{stretchedMasterKey.key.b64}}</pre>
            <h3>Encryption Key</h3>
            <pre>{{stretchedMasterKey.encKey.b64}}</pre>
            <h3>MAC Key</h3>
            <pre>{{stretchedMasterKey.macKey.b64}}</pre>
        </section>

        <section>
            <h2>Generated Symmetric Key</h2>
            <pre>{{symKey.key.b64}}</pre>
            <h3>Encryption Key</h3>
            <pre>{{symKey.encKey.b64}}</pre>
            <h3>MAC Key</h3>
            <pre>{{symKey.macKey.b64}}</pre>
            <h3>Protected Symmetric Key</h3>
            <pre>{{protectedSymKey.string}}</pre>
        </section>

        <section>
            <h2>Generated RSA Key Pair</h2>
            <h3>Public Key</h3>
            <pre>{{publicKey.b64}}</pre>
            <h3>Private Key</h3>
            <pre>{{privateKey.b64}}</pre>
            <h3>Protected Private Key</h3>
            <pre>{{protectedPrivateKey.string}}</pre>
        </section>

        <button type="button" id="deriveKeys" class="btn btn-primary" v-on:click="generateKeys">
            <i class="fa fa-refresh"></i> Regenerate Keys
        </button>

        <h1>Encryption</h1>

        <form>
            <div class="form-group">
                <label for="secret">Secret Value</label>
                <textarea id="secret" class="form-control" v-model="secret"></textarea>
            </div>
        </form>

        <h2>The "Cipher String"</h2>
        <pre>{{protectedSecret.string}}</pre>

        <h2>Decrypt</h2>
        <textarea class="form-control" v-model="unprotectedSecret" readonly></textarea>
    </div>

    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
    <script src="https://unpkg.com/vue"></script>

    <script>
        (function () {
            // Constants/Enums

            const encTypes = {
                AesCbc256_B64: 0,
                AesCbc128_HmacSha256_B64: 1,
                AesCbc256_HmacSha256_B64: 2,
                Rsa2048_OaepSha256_B64: 3,
                Rsa2048_OaepSha1_B64: 4,
                Rsa2048_OaepSha256_HmacSha256_B64: 5,
                Rsa2048_OaepSha1_HmacSha256_B64: 6
            };

            // Object Classes

            class Cipher {
                constructor(encType, iv, ct, mac) {
                    if (!arguments.length) {
                        this.encType = null;
                        this.iv = null;
                        this.ct = null;
                        this.mac = null;
                        this.string = null;
                        return;
                    }

                    this.encType = encType;
                    this.iv = iv;
                    this.ct = ct;
                    this.string = encType + '.' + iv.b64 + '|' + ct.b64;

                    this.mac = null;
                    if (mac) {
                        this.mac = mac;
                        this.string += ('|' + mac.b64);
                    }
                }
            }

            class ByteData {
                constructor(buf) {
                    if (!arguments.length) {
                        this.arr = null;
                        this.b64 = null;
                        return;
                    }

                    this.arr = new Uint8Array(buf);
                    this.b64 = toB64(buf);
                }
            }

            class SymmetricCryptoKey {
                constructor(buf) {
                    if (!arguments.length) {
                        this.key = new ByteData();
                        this.encKey = new ByteData();
                        this.macKey = new ByteData();
                        return;
                    }

                    this.key = new ByteData(buf);

                    // First half
                    const encKey = this.key.arr.slice(0, this.key.arr.length / 2).buffer;
                    this.encKey = new ByteData(encKey);

                    // Second half
                    const macKey = this.key.arr.slice(this.key.arr.length / 2).buffer;
                    this.macKey = new ByteData(macKey);
                }
            }

            // Helpers

            function fromUtf8(str) {
                const strUtf8 = unescape(encodeURIComponent(str));
                const bytes = new Uint8Array(strUtf8.length);
                for (let i = 0; i < strUtf8.length; i++) {
                    bytes[i] = strUtf8.charCodeAt(i);
                }
                return bytes.buffer;
            }

            function toUtf8(buf) {
                const bytes = new Uint8Array(buf);
                const encodedString = String.fromCharCode.apply(null, bytes);
                return decodeURIComponent(escape(encodedString));;
            }

            function toB64(buf) {
                let binary = '';
                const bytes = new Uint8Array(buf);
                for (let i = 0; i < bytes.byteLength; i++) {
                    binary += String.fromCharCode(bytes[i]);
                }
                return window.btoa(binary);
            }

            function hasValue(str) {
                return str && str !== '';
            }

            // Crypto

            async function pbkdf2(password, salt, iterations, length) {
                const importAlg = {
                    name: 'PBKDF2'
                };

                const deriveAlg = {
                    name: 'PBKDF2',
                    salt: salt,
                    iterations: iterations,
                    hash: { name: 'SHA-256' }
                };

                const aesOptions = {
                    name: 'AES-CBC',
                    length: length
                };

                try {
                    const importedKey = await window.crypto.subtle.importKey(
                        'raw', password, importAlg, false, ['deriveKey']);
                    const derivedKey = await window.crypto.subtle.deriveKey(
                        deriveAlg, importedKey, aesOptions, true, ['encrypt']);
                    const exportedKey = await window.crypto.subtle.exportKey('raw', derivedKey);
                    return new ByteData(exportedKey);
                } catch (err) {
                    console.log(err);
                }
            }

            async function aesEncrypt(data, encKey, macKey) {
                const keyOptions = {
                    name: 'AES-CBC'
                };

                const encOptions = {
                    name: 'AES-CBC',
                    iv: new Uint8Array(16)
                };
                window.crypto.getRandomValues(encOptions.iv);
                const ivData = new ByteData(encOptions.iv.buffer);

                try {
                    const importedKey = await window.crypto.subtle.importKey(
                        'raw', encKey.arr.buffer, keyOptions, false, ['encrypt']);
                    const encryptedBuffer = await window.crypto.subtle.encrypt(encOptions, importedKey, data);
                    const ctData = new ByteData(encryptedBuffer);
                    let type = encTypes.AesCbc256_B64;
                    let macData;
                    if (macKey) {
                        const dataForMac = buildDataForMac(ivData.arr, ctData.arr);
                        const macBuffer = await computeMac(dataForMac.buffer, macKey.arr.buffer);
                        type = encTypes.AesCbc256_HmacSha256_B64;
                        macData = new ByteData(macBuffer);
                    }
                    return new Cipher(type, ivData, ctData, macData);
                } catch (err) {
                    console.error(err);
                }
            }

            async function aesDecrypt(cipher, encKey, macKey) {
                const keyOptions = {
                    name: 'AES-CBC'
                };

                const decOptions = {
                    name: 'AES-CBC',
                    iv: cipher.iv.arr.buffer
                };

                try {
                    const checkMac = cipher.encType != encTypes.AesCbc256_B64;
                    if (checkMac) {
                        if (!macKey) {
                            throw 'MAC key not provided.';
                        }
                        const dataForMac = buildDataForMac(cipher.iv.arr, cipher.ct.arr);
                        const macBuffer = await computeMac(dataForMac.buffer, macKey.arr.buffer);
                        const macsMatch = await macsEqual(cipher.mac.arr.buffer, macBuffer, macKey.arr.buffer);
                        if (!macsMatch) {
                            throw 'MAC check failed.';
                        }
                        const importedKey = await window.crypto.subtle.importKey(
                            'raw', encKey.arr.buffer, keyOptions, false, ['decrypt']);
                        return window.crypto.subtle.decrypt(decOptions, importedKey, cipher.ct.arr.buffer);
                    }
                } catch (err) {
                    console.error(err);
                }
            }

            async function computeMac(data, key) {
                const alg = {
                    name: 'HMAC',
                    hash: { name: 'SHA-256' }
                };
                const importedKey = await window.crypto.subtle.importKey('raw', key, alg, false, ['sign']);
                return window.crypto.subtle.sign(alg, importedKey, data);
            }

            async function macsEqual(mac1Data, mac2Data, key) {
                const alg = {
                    name: 'HMAC',
                    hash: { name: 'SHA-256' }
                };

                const importedMacKey = await window.crypto.subtle.importKey('raw', key, alg, false, ['sign']);
                const mac1 = await window.crypto.subtle.sign(alg, importedMacKey, mac1Data);
                const mac2 = await window.crypto.subtle.sign(alg, importedMacKey, mac2Data);

                if (mac1.byteLength !== mac2.byteLength) {
                    return false;
                }

                const arr1 = new Uint8Array(mac1);
                const arr2 = new Uint8Array(mac2);

                for (let i = 0; i < arr2.length; i++) {
                    if (arr1[i] !== arr2[i]) {
                        return false;
                    }
                }

                return true;
            }

            function buildDataForMac(ivArr, ctArr) {
                const dataForMac = new Uint8Array(ivArr.length + ctArr.length);
                dataForMac.set(ivArr, 0);
                dataForMac.set(ctArr, ivArr.length);
                return dataForMac;
            }

            async function generateRsaKeyPair() {
                const rsaOptions = {
                    name: 'RSA-OAEP',
                    modulusLength: 2048,
                    publicExponent: new Uint8Array([0x01, 0x00, 0x01]), // 65537
                    hash: { name: 'SHA-1' }
                };

                try {
                    const keyPair = await window.crypto.subtle.generateKey(rsaOptions, true, ['encrypt', 'decrypt']);
                    const publicKey = new ByteData(await window.crypto.subtle.exportKey('spki', keyPair.publicKey));
                    const privateKey = new ByteData(await window.crypto.subtle.exportKey('pkcs8', keyPair.privateKey));
                    return {
                        publicKey: publicKey,
                        privateKey: privateKey
                    };
                } catch (err) {
                    console.error(err);
                }
            }

            async function stretchKey(key) {
                const newKey = new Uint8Array(64);
                newKey.set(await hkdfExpand(key, new Uint8Array(fromUtf8('enc')), 32));
                newKey.set(await hkdfExpand(key, new Uint8Array(fromUtf8('mac')), 32), 32);
                return new SymmetricCryptoKey(newKey.buffer);
            }

            // ref: https://tools.ietf.org/html/rfc5869
            async function hkdfExpand(prk, info, size) {
                const alg = {
                    name: 'HMAC',
                    hash: { name: 'SHA-256' }
                };
                const importedKey = await window.crypto.subtle.importKey('raw', prk, alg, false, ['sign']);
                const hashLen = 32; // sha256
                const okm = new Uint8Array(size);
                let previousT = new Uint8Array(0);
                const n = Math.ceil(size / hashLen);
                for (let i = 0; i < n; i++) {
                    const t = new Uint8Array(previousT.length + info.length + 1);
                    t.set(previousT);
                    t.set(info, previousT.length);
                    t.set([i + 1], t.length - 1);
                    previousT = new Uint8Array(await window.crypto.subtle.sign(alg, importedKey, t.buffer));
                    okm.set(previousT, i * hashLen);
                }
                return okm;
            }

            // App

            const vm = new Vue({
                el: '#app',
                data: {
                    email: '',
                    masterPassword: '',
                    pbkdf2Iterations: 100000,

                    masterKey: new ByteData(),
                    masterKeyHash: new ByteData(),
                    stretchedMasterKey: new SymmetricCryptoKey(),

                    symKey: new SymmetricCryptoKey(),
                    protectedSymKey: new Cipher(),
                    unprotectedSymKey: new ByteData(),

                    publicKey: new ByteData(),
                    privateKey: new ByteData(),
                    protectedPrivateKey: new Cipher(),
                    unprotectedPrivateKey: new ByteData(),

                    secret: '',
                    protectedSecret: new Cipher(),
                    unprotectedSecret: ''
                },
                computed: {
                    masterPasswordBuffer() {
                        return this.masterPassword ? fromUtf8(this.masterPassword) : null;
                    },
                    emailBuffer() {
                        return this.email ? fromUtf8(this.email) : null;
                    },
                    secretBuffer() {
                        return this.secret ? fromUtf8(this.secret) : null;
                    }
                },
                watch: {
                    async masterKey(newValue) {
                        const self = this;

                        if (!newValue || !newValue.arr || !self.masterPasswordBuffer) {
                            return new ByteData();
                        }

                        self.stretchedMasterKey = await stretchKey(newValue.arr.buffer);
                        self.masterKeyHash = await pbkdf2(newValue.arr.buffer, self.masterPasswordBuffer, 1, 256);
                    }
                },
                methods: {
                    async generateKeys() {
                        const self = this;

                        const symKey = new Uint8Array(512 / 8);
                        window.crypto.getRandomValues(symKey);
                        self.symKey = new SymmetricCryptoKey(symKey);

                        const keyPair = await generateRsaKeyPair();
                        self.publicKey = keyPair.publicKey;
                        self.privateKey = keyPair.privateKey;
                    }
                }
            });

            vm.$watch(() => {
                return {
                    masterPassword: vm.masterPasswordBuffer,
                    email: vm.emailBuffer,
                    iterations: vm.pbkdf2Iterations
                };
            }, async (newVal, oldVal) => {
                if (!newVal.masterPassword || !newVal.email || !newVal.iterations || newVal.iterations < 1) {
                    vm.masterKey = new ByteData();
                    return;
                }

                vm.masterKey = await pbkdf2(newVal.masterPassword, newVal.email, newVal.iterations, 256);
            });

            vm.$watch(() => {
                return {
                    symKey: vm.symKey,
                    secret: vm.secretBuffer
                };
            }, async (newVal, oldVal) => {
                if (!newVal.symKey || !newVal.secret) {
                    vm.protectedSecret = new Cipher();
                    vm.unprotectedSecret = '';
                    return;
                }

                vm.protectedSecret = await aesEncrypt(newVal.secret, newVal.symKey.encKey, newVal.symKey.macKey);
                const secret = await aesDecrypt(vm.protectedSecret, newVal.symKey.encKey, newVal.symKey.macKey);
                vm.unprotectedSecret = toUtf8(secret);
            });

            vm.$watch(() => {
                return {
                    stretchedMasterKey: vm.stretchedMasterKey,
                    symKey: vm.symKey
                };
            }, async (newVal, oldVal) => {
                if (!newVal.stretchedMasterKey || !newVal.stretchedMasterKey.key ||
                    !newVal.stretchedMasterKey.key.arr || !newVal.symKey || !newVal.symKey.key ||
                    !newVal.symKey.key.arr) {
                    vm.protectedSymKey = new Cipher();
                    return;
                }

                vm.protectedSymKey = await aesEncrypt(newVal.symKey.key.arr, newVal.stretchedMasterKey.encKey,
                    newVal.stretchedMasterKey.macKey);
                const unprotectedSymKey = await aesDecrypt(vm.protectedSymKey, newVal.stretchedMasterKey.encKey,
                    newVal.stretchedMasterKey.macKey);
                vm.unprotectedSymKey = new ByteData(unprotectedSymKey);
            });

            vm.$watch(() => {
                return {
                    symKey: vm.symKey,
                    privateKey: vm.privateKey
                };
            }, async (newVal, oldVal) => {
                if (!newVal.symKey || !newVal.symKey.key || !newVal.privateKey || !newVal.privateKey.arr) {
                    vm.protectedPrivateKey = new Cipher();
                    return;
                }

                vm.protectedPrivateKey = await aesEncrypt(newVal.privateKey.arr, newVal.symKey.encKey,
                    newVal.symKey.macKey);
                const unprotectedPrivateKey = await aesDecrypt(vm.protectedPrivateKey, newVal.symKey.encKey,
                    newVal.symKey.macKey);
                vm.unprotectedPrivateKey = new ByteData(unprotectedPrivateKey);
            });

            // Set default values
            vm.email = 'user@example.com';
            vm.masterPassword = 'password123';
            vm.secret = 'This is a secret.';

            // Generate initial set of keys
            vm.generateKeys();
        })();
    </script>
</body>

</html>